package com.bkck.sharkblockade.levels.shark;

import com.bkck.sharkblockade.levels.ball.Ball;
import com.bkck.sharkblockade.levels.harpoon.Harpoon;
import com.bkck.sharkblockade.levels.lines.Line;
import com.bkck.sharkblockade.levels.lines.Point;
import com.bkck.sharkblockade.levels.thread.MainGamePanel;
import com.bkck.sharkblockade.levels.thread.MainThread;
import android.graphics.Bitmap;
import android.graphics.Canvas;

public class Shark {

  public final static int BLUE = 0;
  public final static int ORANGE = 1;

  // the speed of the shark
  private float pixelsPerMeter = 2.5f;
  // time of last update in milliseconds
  private long lastUpdateTime = -1;

  private Bitmap bitmap;
  private int sharkHeight;
  private int sharkWidth;

  private float x;
  private float y;
  private float originalX;
  private float originalY;

  private float previousX;
  private float previousY;

  // speed in meters per second
  // should be constant for sharks unless a powerup.
  private float xVelocity;
  private float originalVelocity;

  private boolean killed;
  private long killedAt = -1;

  private int color;

  private boolean confused = false;

  public Line headTopLine;
  public Line headBottomLine;
  public Line headVerticalLine;
  public Line tailTopLine;
  public Line tailBottomLine;
  public Line tailVerticalLine;
  public Line bottomLine;
  public Line leftLine;
  public Line rightLine;

  /**
   * Shark constructor
   * 
   * @param bitmap The bitmap for the shark.
   * @param x The x-coordinate of the shark.
   * @param y The y-coordinate of the shark.
   * @param direction 0 means moving to the left, 1 means moving to the right.
   */
  public Shark(Bitmap bitmap, int x, int y, int xVelocity, int color) {
    this.bitmap = bitmap;
    this.sharkHeight = bitmap.getHeight();
    this.sharkWidth = bitmap.getWidth();
    this.x = x;
    this.y = y;
    originalX = x;
    originalY = y;
    this.xVelocity = xVelocity;
    originalVelocity = xVelocity;
    this.color = color;
    killed = false;

    if (color == ORANGE) {
      headTopLine = new Line(x + sharkWidth * 2 / 3, y, x + sharkWidth, y);
      headBottomLine =
          new Line(x + sharkWidth * 2 / 3, y + sharkHeight, x + sharkWidth, y + sharkHeight);
      headVerticalLine = new Line(x + sharkWidth, y, x + sharkWidth, y + sharkHeight);
      tailTopLine = new Line(x, y, x + sharkWidth * 2 / 3, y);
      tailBottomLine = new Line(x, y + sharkHeight, x + sharkWidth * 2 / 3, y + sharkHeight);
      tailVerticalLine = new Line(x, y, x, y + sharkHeight);
    }
    else {
      tailTopLine = new Line(x + sharkWidth / 3, y, x + sharkWidth, y);
      tailBottomLine =
          new Line(x + sharkWidth / 3, y + sharkHeight, x + sharkWidth, y + sharkHeight);
      tailVerticalLine = new Line(x + sharkWidth, y, x + sharkWidth, y + sharkHeight);
      headTopLine = new Line(x, y, x + sharkWidth / 3, y);
      headBottomLine = new Line(x, y + sharkHeight, x + sharkWidth / 3, y + sharkHeight);
      headVerticalLine = new Line(x, y, x, y + sharkHeight);
    }

    bottomLine = new Line(x, y + sharkHeight, x + sharkWidth, y + sharkHeight);
    leftLine = new Line(x, y, x, y + sharkHeight);
    rightLine = new Line(x + sharkWidth, y, x + sharkWidth, y + sharkHeight);
  }

  /**
   * Empty constructor to be called by Activities.
   */
  public Shark() {

  }

  /**
   * Whether the shark is confused by ghost ball.
   * 
   * @return confused.
   */
  public boolean isConfused() {
    return confused;
  }

  /**
   * Set the speed for the shark.
   * 
   * @param newPixelsPerMeter
   */
  public void setPixelsPerMeter(float pixelsPerMeter) {
    this.pixelsPerMeter = pixelsPerMeter;
  }

  /**
   * Return the speed of the shark.
   * 
   * @return
   */
  public float getPixelsPerMeter() {
    return pixelsPerMeter;
  }

  /**
   * Get this shark's color
   * 
   * @return 0 - blue 1 - orange
   */
  public int getColor() {
    return color;
  }

  /**
   * Set this sharks color
   * 
   * @param color
   */
  public void setColor(int color) {
    this.color = color;
  }

  /**
   * Return the last update time.
   * 
   * @return The last update time.
   */
  public long getLastUpdateTime() {
    return lastUpdateTime;
  }

  /**
   * Set the last update time.
   * 
   * @param lastUpdateTime
   */
  public void setLastUpdateTime(long lastUpdateTime) {
    this.lastUpdateTime = lastUpdateTime;
  }

  /**
   * Return the bitmap for this shark.
   * 
   * @return The bitmap.
   */
  public Bitmap getBitmap() {
    return bitmap;
  }

  /**
   * Set this shark's bitmap.
   * 
   * @param bitmap The bitmap.
   */
  public void setBitmap(Bitmap bitmap) {
    this.bitmap = bitmap;
    sharkHeight = bitmap.getHeight();
    sharkWidth = bitmap.getWidth();
  }

  /**
   * Return this shark's height.
   * 
   * @return The height in pixels.
   */
  public int getHeight() {
    return sharkHeight;
  }

  /**
   * Return this shark's width.
   * 
   * @return The width in pixels.
   */
  public int getWidth() {
    return sharkWidth;
  }

  /**
   * Return this shark's x coordinate.
   * 
   * @return The x coordinate.
   */
  public float getX() {
    return x;
  }

  /**
   * Set this shark's x coordinate.
   * 
   * @param x The x coordinate.
   */
  public void setX(float x) {
    previousX = this.x;
    this.x = x;
  }

  /**
   * Return the shark's y coordinate.
   * 
   * @return The y coordinate.
   */
  public float getY() {
    return y;
  }

  /**
   * Set the shark's y coordinate.
   * 
   * @param y The y coordinate.
   */
  public void setY(float y) {
    previousY = this.y;
    this.y = y;
  }

  /**
   * Return the previous x coordinate of this shark.
   * 
   * @return previousX
   */
  public float getPreviousX() {
    return previousX;
  }

  /**
   * Set the previous x coordinate of this shark.
   * 
   * @param previousX
   */
  public void setPreviousX(float previousX) {
    this.previousX = previousX;
  }

  /**
   * Return the previous y coordinate of this shark.
   * 
   * @return previousY
   */
  public float getPreviousY() {
    return previousY;
  }

  /**
   * Set the previous y coordinate of this shark.
   * 
   * @param previousY
   */
  public void setPreviousY(float previousY) {
    this.previousY = previousY;
  }

  /**
   * Return the x velocity of this shark.
   * 
   * @return The x velocity.
   */
  public float getXVelocity() {
    return xVelocity;
  }

  /**
   * Set the x velocity of this shark.
   * 
   * @param xVelocity THe x velocity.
   */
  public void setXVelocity(float xVelocity) {
    this.xVelocity = xVelocity;
  }

  /**
   * Draw the ball to the canvas.
   * 
   * @param canvas
   */
  public void draw(Canvas canvas) {
    canvas.drawBitmap(bitmap, x, y, null);
  }

  public void killed() {
    this.killedAt = System.currentTimeMillis();
    if (color == BLUE) {
      MainGamePanel.setBlueKilledAt(killedAt);
    }
    if (color == ORANGE) {
      MainGamePanel.setOrangeKilledAt(killedAt);
    }
    this.x = -100;
    this.y = -100;
    this.xVelocity = 0;
    this.killed = true;
    MainThread.addScore(500);
  }

  public void respawnShark() {
    this.x = originalX;
    this.y = originalY;
    this.xVelocity = originalVelocity;
    this.killed = false;
    if (color == BLUE) {
      MainGamePanel.setBlueKilledAt(-1);
    }
    if (color == ORANGE) {
      MainGamePanel.setOrangeKilledAt(-1);
    }
  }

  /**
   * Updates the shark position and checks if ball hit shark.
   * 
   * @param viewHeight - height of the window.
   * @param viewWidth - width of the window.
   * @param ball - The ball
   */
  public void updatePosition(Ball ball, EatBall eatBall) {
    long currentTime = System.currentTimeMillis();
    if (!this.killed) {
      if (lastUpdateTime < 0) {
        lastUpdateTime = currentTime;
        return;
      }

      long elapsedTime = currentTime - lastUpdateTime;
      lastUpdateTime = currentTime;

      previousX = x;
      previousY = y;

      sharkMove(elapsedTime);

      // check the collision between the shark and the ball
      if (isCloseTo(ball)) {
        if (headCollideWith(ball)) {
          if (!ball.getGhost()) {
            eatBall(ball, eatBall);
            ball.setEaten(true);
            ball.setLastUpdateTime(-1);
          }
          else {
            confused = true;
          }
          return;
        }
        if (!ball.getGhost()) {
          checkTailCollision(ball);
        }
      }
      else {
        confused = false;
      }
    }
    else {
      if (currentTime - this.killedAt >= 40000) {
        respawnShark();
      }
    }
  }

  public boolean isCloseTo(Ball ball) {
    // do some basic checks beforehand to avoid doing complex line intersection calculation if
    // possible
    // if ball is above the shark
    if (ball.getPreviousY() + ball.getHeight() < y && ball.getY() + ball.getHeight() < y) {
      return false;
    }
    // if ball is below the shark
    if (ball.getPreviousY() > y + sharkHeight && ball.getY() > y + sharkHeight) {
      return false;
    }
    // if ball is to the right of the shark
    if (ball.getPreviousX() > x + sharkWidth && ball.getX() > x + sharkWidth) {
      return false;
    }
    // if ball is to the left of the shark
    if (ball.getPreviousX() + ball.getWidth() < x && ball.getX() + ball.getWidth() < x) {
      return false;
    }
    return true;
  }

  public boolean isCloseTo(Harpoon harpoon) {
    // do some basic checks beforehand to avoid doing complex line intersection calculation if
    // possible
    // if ball is above the shark
    if (harpoon.getPreviousY() + harpoon.getHeight() < y
        && harpoon.getY() + harpoon.getHeight() < y) {
      return false;
    }
    // if ball is below the shark
    if (harpoon.getPreviousY() > y + sharkHeight && harpoon.getY() > y + sharkHeight) {
      return false;
    }
    // if ball is to the right of the shark
    if (harpoon.getX() > x + sharkWidth) {
      return false;
    }
    // if ball is to the left of the shark
    if (harpoon.getX() + harpoon.getWidth() < x) {
      return false;
    }
    return true;
  }

  /**
   * Checks the collision between the ball and the shark's tail. The ball should bounce off of the
   * tail if a collision was detected.
   * 
   * @param ball
   */
  public void checkTailCollision(Ball ball) {

    Point intersection = null;

    // ball going down, check top of tail
    if (ball.getYVelocity() > 0) {
      // bottom left corner of the ball
      intersection =
          ball.findIntersection(tailTopLine, ball.getPreviousX(),
              ball.getPreviousY() + ball.getHeight(), ball.getX(), ball.getY() + ball.getHeight());
      if (intersection != null) {
        ball.setX(intersection.getX());
        ball.setY(intersection.getY() - ball.getHeight());
        ball.setYVelocity(ball.getYVelocity() * -1);
        return;
      }

      // bottom right corner of the ball
      intersection =
          ball.findIntersection(tailTopLine, ball.getPreviousX() + ball.getWidth(),
              ball.getPreviousY() + ball.getHeight(), ball.getX() + ball.getWidth(), ball.getY()
                  + ball.getHeight());
      if (intersection != null) {
        ball.setX(intersection.getX() - ball.getWidth());
        ball.setY(intersection.getY() - ball.getHeight());
        ball.setYVelocity(ball.getYVelocity() * -1);
        return;
      }
    }
    // ball going up, check bottom of tail
    if (ball.getYVelocity() < 0) {
      // check top left of the ball
      intersection =
          ball.findIntersection(tailBottomLine, ball.getPreviousX(), ball.getPreviousY(),
              ball.getX(), ball.getY());
      if (intersection != null) {
        ball.setX(intersection.getX());
        ball.setY(intersection.getY());
        ball.setYVelocity(ball.getYVelocity() * -1);
        return;
      }

      // check top right corner of the ball
      intersection =
          ball.findIntersection(tailBottomLine, ball.getPreviousX() + ball.getWidth(),
              ball.getPreviousY(), ball.getX() + ball.getWidth(), ball.getY());
      if (intersection != null) {
        ball.setX(intersection.getX() - ball.getWidth());
        ball.setY(intersection.getY());
        ball.setYVelocity(ball.getYVelocity() * -1);
        return;
      }
    }
    // shark going right and the ball moving to shark's tail end
    if (xVelocity > 0 && ball.getXVelocity() > 0) {
      // bottom right corner of the ball
      intersection =
          ball.findIntersection(tailVerticalLine, ball.getPreviousX() + ball.getWidth(),
              ball.getPreviousY() + ball.getHeight(), ball.getX() + ball.getWidth(), ball.getY()
                  + ball.getHeight());
      if (intersection != null) {
        ball.setX(intersection.getX() - ball.getWidth());
        ball.setY(intersection.getY() - ball.getHeight());
        ball.setXVelocity(ball.getXVelocity() * -1);
        return;
      }

      // check top right corner of the ball
      intersection =
          ball.findIntersection(tailVerticalLine, ball.getPreviousX() + ball.getWidth(),
              ball.getPreviousY(), ball.getX() + ball.getWidth(), ball.getY());
      if (intersection != null) {
        ball.setX(intersection.getX() - ball.getWidth());
        ball.setY(intersection.getY());
        ball.setXVelocity(ball.getXVelocity() * -1);
        return;
      }
    }
    // shark going left and the ball moving to shark's tail end
    if (xVelocity < 0 && ball.getXVelocity() < 0) {
      // check top left of the ball
      intersection =
          ball.findIntersection(tailVerticalLine, ball.getPreviousX(), ball.getPreviousY(),
              ball.getX(), ball.getY());
      if (intersection != null) {
        ball.setX(intersection.getX());
        ball.setY(intersection.getY());
        ball.setXVelocity(ball.getXVelocity() * -1);
        return;
      }

      // bottom left corner of the ball
      intersection =
          ball.findIntersection(tailVerticalLine, ball.getPreviousX(),
              ball.getPreviousY() + ball.getHeight(), ball.getX(), ball.getY() + ball.getHeight());
      if (intersection != null) {
        ball.setX(intersection.getX());
        ball.setY(intersection.getY() - ball.getHeight());
        ball.setXVelocity(ball.getXVelocity() * -1);
        return;
      }
    }

  }

  /**
   * Whether the ball is hitting the shark's head.
   * 
   * @param ball
   * @return
   */
  public boolean headCollideWith(Ball ball) {

    if (ball.hasIntersection(headTopLine, ball.getPreviousX(),
        ball.getPreviousY() + ball.getHeight(), ball.getX(), ball.getY() + ball.getHeight())
        && ball.hasIntersection(headTopLine, ball.getPreviousX() + ball.getWidth(),
            ball.getPreviousY() + ball.getHeight(), ball.getX() + ball.getWidth(), ball.getY()
                + ball.getHeight())) {
      return true;
    }
    if (ball.hasIntersection(headBottomLine, ball.getPreviousX(), ball.getPreviousY(), ball.getX(),
        ball.getY())
        && ball.hasIntersection(headBottomLine, ball.getPreviousX() + ball.getWidth(),
            ball.getPreviousY(), ball.getX() + ball.getWidth(), ball.getY())) {
      return true;
    }
    if (ball.hasIntersection(headVerticalLine, ball.getPreviousX(),
        ball.getPreviousY() + ball.getHeight(), ball.getX(), ball.getY() + ball.getHeight())
        || ball.hasIntersection(headVerticalLine, ball.getPreviousX() + ball.getWidth(),
            ball.getPreviousY() + ball.getHeight(), ball.getX() + ball.getWidth(), ball.getY()
                + ball.getHeight())
        || ball.hasIntersection(headVerticalLine, ball.getPreviousX(), ball.getPreviousY(),
            ball.getX(), ball.getY())
        || ball.hasIntersection(headVerticalLine, ball.getPreviousX() + ball.getWidth(),
            ball.getPreviousY(), ball.getX() + ball.getWidth(), ball.getY())) {
      return true;
    }

    return false;
  }

  /**
   * Check to see if harpoon hit shark.
   * 
   * @param shark
   * @return Whether it hit shark or not.
   */
  public boolean hitShark(Harpoon harpoon) {
    updateSharkEdges();
    Point intersection = null;

    // intersection between left of shark and harpoon top right
    intersection =
        findIntersection(leftLine, harpoon.getX() + harpoon.getWidth(), harpoon.getPreviousY(),
            harpoon.getX() + harpoon.getWidth(), harpoon.getY());
    if (intersection != null) {
      return true;
    }

    // intersection between left of shark and harpoon bottom right
    intersection =
        findIntersection(leftLine, harpoon.getX() + harpoon.getWidth(), harpoon.getPreviousY()
            + harpoon.getHeight(), harpoon.getX() + harpoon.getWidth(),
            harpoon.getY() + harpoon.getHeight());
    if (intersection != null) {
      return true;
    }

    // intersection between right of shark and harpoon top left
    intersection =
        findIntersection(rightLine, harpoon.getX(), harpoon.getPreviousY(), harpoon.getX(),
            harpoon.getY());
    if (intersection != null) {
      return true;
    }

    // intersection between right of shark and harpoon bottom left
    intersection =
        findIntersection(rightLine, harpoon.getX(), harpoon.getPreviousY() + harpoon.getHeight(),
            harpoon.getX(), harpoon.getY() + harpoon.getHeight());
    if (intersection != null) {
      return true;
    }

    // intersection between bottom of shark and harpoon top left
    intersection =
        findIntersection(bottomLine, harpoon.getX(), harpoon.getPreviousY(), harpoon.getX(),
            harpoon.getY());
    if (intersection != null) {
      return true;
    }

    // intersection between bottom of shark and harpoon top right
    intersection =
        findIntersection(bottomLine, harpoon.getX() + harpoon.getWidth(), harpoon.getPreviousY(),
            harpoon.getX() + harpoon.getWidth(), harpoon.getY());
    if (intersection != null) {
      return true;
    }

    return false;
  }

  /**
   * Lose a life and reset the ball when it's eaten by this shark.
   * 
   * @param ball The ball.
   * @param eatAnimation The eating animation.
   */
  public void eatBall(Ball ball, EatBall eatAnimation) {
    lastUpdateTime = -1;

    // hide the ball..at the end of the eatAnimation we'll reset it on the paddle

    ball.setEaten(true);
    ball.setX(0);
    ball.setY(MainGamePanel.viewHeight);
    ball.setXVelocity(0);
    ball.setYVelocity(0);

    // stop to eat the ball
    xVelocity = 0;
    eatAnimation.startEating(this);

    MainThread.loseOneLife();
    // ball.reset();
  }

  /**
   * Moves the shark based on which direction it is moving.
   */
  public void sharkMove(long elapsedTime) {
    if (xVelocity < 0) {
      // Shark moves from right to left.
      // if the shark is at the left wall.
      if (x + sharkWidth <= 0) {
        x = MainGamePanel.viewWidth;
      }
      else {
        // change the shark coordinate according to the velocities
        x = x + xVelocity * elapsedTime / 1000 * pixelsPerMeter;
      }
    }
    else {
      // Shark moves from left to right.
      // if the shark is at the right wall.
      if (x >= MainGamePanel.viewWidth) {
        x = 0 - sharkWidth;
      }
      else {
        // change the shark coordinate according to the velocities
        x = x + xVelocity * elapsedTime / 1000 * pixelsPerMeter;
      }
    }

    updateSharkEdges();
  }

  /**
   * Updates the lines around the shark's head and tail to its current location.
   */
  public void updateSharkEdges() {
    if (color == ORANGE) {
      headTopLine.getPoint1().setX(x + sharkWidth / 2);
      headTopLine.getPoint2().setX(x + sharkWidth);
      headBottomLine.getPoint1().setX(x + sharkWidth / 2);
      headBottomLine.getPoint2().setX(x + sharkWidth);
      headVerticalLine.getPoint1().setX(x + sharkWidth);
      headVerticalLine.getPoint2().setX(x + sharkWidth);
      tailTopLine.getPoint1().setX(x);
      tailTopLine.getPoint2().setX(x + sharkWidth / 2);
      tailBottomLine.getPoint1().setX(x);
      tailBottomLine.getPoint2().setX(x + sharkWidth / 2);
      tailVerticalLine.getPoint1().setX(x);
      tailVerticalLine.getPoint2().setX(x);
    }
    else {
      tailTopLine.getPoint1().setX(x + sharkWidth / 2);
      tailTopLine.getPoint2().setX(x + sharkWidth);
      tailBottomLine.getPoint1().setX(x + sharkWidth / 2);
      tailBottomLine.getPoint2().setX(x + sharkWidth);
      tailVerticalLine.getPoint1().setX(x + sharkWidth);
      tailVerticalLine.getPoint2().setX(x + sharkWidth);
      headTopLine.getPoint1().setX(x);
      headTopLine.getPoint2().setX(x + sharkWidth / 2);
      headBottomLine.getPoint1().setX(x);
      headBottomLine.getPoint2().setX(x + sharkWidth / 2);
      headVerticalLine.getPoint1().setX(x);
      headVerticalLine.getPoint2().setX(x);
    }

    bottomLine.getPoint1().setX(x);
    bottomLine.getPoint1().setY(y + sharkHeight);
    bottomLine.getPoint2().setX(x + sharkWidth);
    bottomLine.getPoint2().setY(y + sharkHeight);
    leftLine.getPoint1().setX(x);
    leftLine.getPoint1().setY(y);
    leftLine.getPoint2().setX(x);
    leftLine.getPoint2().setY(y + sharkHeight);
    rightLine.getPoint1().setX(x + sharkWidth);
    rightLine.getPoint1().setY(y);
    rightLine.getPoint2().setX(x + sharkWidth);
    rightLine.getPoint2().setY(y + sharkHeight);
  }

  /**
   * Resets the shark's position to where they first started.
   */
  public void reset() {
    killed = false;
    lastUpdateTime = -1;
    if (color == BLUE) {
      x = MainGamePanel.viewWidth;
      y = MainGamePanel.BLUE_SHARK_HEIGHT;
      xVelocity = -10;
    }
    if (color == ORANGE) {
      x = 0 - sharkWidth;
      y = MainGamePanel.ORANGE_SHARK_HEIGHT;
      xVelocity = 10;
    }
  }

  public long getKilledAt() {
    return this.killedAt;
  }

  public void setKilledAt(long killed) {
    this.killedAt = killed;
  }

  /**
   * Find the intersection of two line segments where second line is represented by two points. (cx,
   * cy) and (dx, dy)
   * 
   * @param line
   * @param cx
   * @param cy
   * @param dx
   * @param dy
   * @return
   */
  public Point findIntersection(Line line, float cx, float cy, float dx, float dy) {
    return findIntersection(line.getPoint1().getX(), line.getPoint1().getY(), line.getPoint2()
        .getX(), line.getPoint2().getY(), cx, cy, dx, dy);
  }

  /**
   * Find the intersection of two line segments. If there's no intersection a null is returned. a,
   * b, c, d are points. Line 1 = (a, b) and Line 2 = (c, d) Source:
   * http://goosebumps4all.net/34all/bb/showthread.php?tid=113
   * 
   * @param ax
   * @param ay
   * @param bx
   * @param by
   * @param cx
   * @param cy
   * @param dx
   * @param dy
   * @return
   */
  public Point findIntersection(float ax, float ay, float bx, float by, float cx, float cy,
      float dx, float dy) {
    float distAB;
    float sinValue;
    float cosValue;
    float newX;
    float posAB;

    // return null if either line has length 0
    if ((ax == bx && ay == by) || (cx == dx && cy == dy)) {
      return null;
    }
    // if the end of the lines intersect
    if (ax == cx && ay == cy || bx == cx && by == cy) {
      return new Point(cx, cy);
    }
    if (ax == dx && ay == dy || bx == dx && by == dy) {
      return new Point(dx, dy);
    }

    // (1) Translate the system so that point A is on the origin.
    bx -= ax;
    by -= ay;
    cx -= ax;
    cy -= ay;
    dx -= ax;
    dy -= ay;

    // Discover the length of segment A-B.
    distAB = (float) Math.sqrt(bx * bx + by * by);

    // (2) Rotate the system so that point B is on the positive X axis.
    cosValue = bx / distAB;
    sinValue = by / distAB;
    newX = cx * cosValue + cy * sinValue;
    cy = cy * cosValue - cx * sinValue;
    cx = newX;
    newX = dx * cosValue + dy * sinValue;
    dy = dy * cosValue - dx * sinValue;
    dx = newX;

    // Fail if segment C-D doesn't cross line A-B.
    if ((cy < 0. && dy < 0.) || (cy >= 0. && dy >= 0.)) {
      return null;
    }

    // (3) Discover the position of the intersection point along line A-B.
    posAB = dx + (cx - dx) * dy / (dy - cy);

    // Fail if segment C-D crosses line A-B outside of segment A-B.
    if (posAB < 0. || posAB > distAB) {
      return null;
    }

    // (4) Apply the discovered position to line A-B in the original coordinate system.
    return new Point(ax + posAB * cosValue, ay + posAB * sinValue);
  }

}
